from pyjvm.exceptions import InstantiationError, NullPointException, \
    IncompatibleClassChangeError, IllegalAccessError, AbstractMethodError, \
    NoSuchMethodError, ClassCastException, NegativeArraySizeException
from pyjvm.instructions.common import Instruction, BytecodeReader, \
    Index16Instruction, ClassInitializer
from pyjvm.instructions.invoke import invoke_method
from pyjvm.runtime.heap import Heap
from pyjvm.runtime.loader import ClassLoader
from pyjvm.runtime.metadata import Class, Method, lookup_method_in_class
from pyjvm.runtime.thread import Frame, OperandStack
from pyjvm.runtime.hack import hacked_invoke_static
from pyjvm.runtime.strpool import StringPool


def goto_init_class(klass: Class, frame: Frame):
    if not klass.inited:
        frame.revert_next_pc()
        ClassInitializer.init_class(frame.thread, klass)
        return True
    return False


class NEW(Index16Instruction):

    def execute(self, frame: Frame):
        cp = frame.method.klass.runtime_const_pool
        class_ref = cp[self.index]
        klass: Class = class_ref.resolved_class()
        if goto_init_class(klass, frame):
            self.record = None
            return

        if klass.is_interface or klass.is_abstract:
            self.record = None
            raise InstantiationError()

        ref = Heap.new_object(klass)
        frame.operand_stack.push(ref)

        self.record = 'NEW class: %s' % klass.name


class INVOKE_STATIC(Index16Instruction):

    def execute(self, frame: Frame):
        cp = frame.method.klass.runtime_const_pool
        method_ref = cp[self.index]
        method = method_ref.resolved_method()
        # print('invoke static: %s %s.%s' % (
        #     method.descriptor, method.klass.name, method.name))
        if method.is_static:
            klass = method.klass
            if goto_init_class(klass, frame):
                self.record = None
                return
        else:
            raise IncompatibleClassChangeError()

        if hacked_invoke_static(method, frame):
            pass
        else:
            invoke_method(frame, method)

        self.record = 'INVOKE_STATIC %s %s.%s(%s)' % (method.return_type,
                                                      method.klass.name, method.name,
                                                      ','.join(method.parameter_types))
        # hacked_invoke_static(method, frame)


class INVOKE_VIRTUAL(Index16Instruction):

    def execute(self, frame: Frame):
        klass = frame.method.klass
        cp = klass.runtime_const_pool
        method_ref = cp[self.index]
        method: Method = method_ref.resolved_method()
        if method.is_static:
            raise IncompatibleClassChangeError()

        ref = frame.operand_stack[-len(method.parameter_types) - 1]

        if ref is None:
            raise NullPointException()

        if method.descriptor == '()Z' and method.klass.name == 'java/lang/Class' and \
                method.name == 'desiredAssertionStatus':
            debug = 1

        # todo 验证
        # if method.is_protected and method.klass.is_super_class_of(klass) and \
        #         method.klass.package_name != klass.package_name and \
        #         ref.klass != klass and not ref.klass.is_sub_class_of(klass):
        #     if not (ref.klass.is_array_class() and method.name == 'clone'):
        #         raise IllegalAccessError()

        if not (ref.klass.is_sub_class_of(method_ref.resolved_class()) or ref.klass.is_implements(
                method_ref.resolved_class())):
            invoking_method = method
        else:
            invoking_method = lookup_method_in_class(
                ref.klass, method_ref.name, method_ref.descriptor)

        if invoking_method is None or invoking_method.is_abstract:
            raise AbstractMethodError()

        invoke_method(frame, invoking_method)

        self.record = 'INVOKE_VIRTUAL %s %s.%s(%s)' % (invoking_method.return_type,
                                                       invoking_method.klass.name, invoking_method.name,
                                                       ','.join(invoking_method.parameter_types))


class NEW_ARRAY(Instruction):
    AT_BOOLEAN = 4
    AT_CHAR = 5
    AT_FLOAT = 6
    AT_DOUBLE = 7
    AT_BYTE = 8
    AT_SHORT = 9
    AT_INT = 10
    AT_LONG = 11

    def __init__(self):
        Instruction.__init__(self)
        self.array_type = None

    def fetch_operands(self, reader: BytecodeReader):
        self.array_type = reader.int8()

    def execute(self, frame: Frame):
        stack = frame.operand_stack
        length = stack.pop()
        loader = frame.method.klass.loader
        array_class = get_array_class(loader, self.array_type)
        ref = Heap.new_array(array_class, length)
        stack.push(ref)


class ANEW_ARRAY(Index16Instruction):

    def execute(self, frame: Frame):
        stack = frame.operand_stack
        cp = frame.method.klass.runtime_const_pool
        klass = cp[self.index].resolved_class()
        length = stack.pop()
        array_class = frame.method.klass.loader.get_component_array_class(
            klass)
        ref = Heap.new_array(array_class, length)
        stack.push(ref)


class ATHROW(Instruction):

    def execute(self, frame: Frame):
        ex_ref = frame.operand_stack.pop()
        if ex_ref is None:
            raise NullPointException()
        thread = frame.thread
        if goto_ex_handler(thread, ex_ref):
            return
        goto_uncaught_handler(thread, ex_ref)


def goto_ex_handler(thread, ex_ref):
    while thread.is_not_empty_frame():
        frame = thread.current_frame()
        pc = frame.next_pc - 1
        handle_pc = frame.method.find_ex_handler_pc(ex_ref.klass, pc)
        if handle_pc is not None and handle_pc > 0:
            stack = frame.operand_stack
            stack.clear()
            stack.push(ex_ref)
            frame.set_next_pc(handle_pc)
            return True
        thread.pop_frame()
    return False


def goto_uncaught_handler(thread, ex):
    thread.clear_stack()
    j_msg = ex.get_field_value('detailMessage', 'Ljava/lang/String;')
    py_msg = StringPool.to_py_str(j_msg)

    print(ex.klass.name.replace('/', '.') + ':' + py_msg)

    if ex.extra is not None:
        for s in ex.extra:
            print('\tat %s' % s)


ARRAY_TYPE_MAP = {
    NEW_ARRAY.AT_BOOLEAN: '[Z',
    NEW_ARRAY.AT_BYTE: '[B',
    NEW_ARRAY.AT_CHAR: '[C',
    NEW_ARRAY.AT_DOUBLE: '[D',
    NEW_ARRAY.AT_FLOAT: '[F',
    NEW_ARRAY.AT_INT: '[I',
    NEW_ARRAY.AT_LONG: '[J',
    NEW_ARRAY.AT_SHORT: '[S',
}


def get_array_class(loader: ClassLoader, array_type):
    return loader.load_class(ARRAY_TYPE_MAP[array_type])


class INVOKE_SPECIAL(Index16Instruction):

    def execute(self, frame: Frame):
        klass = frame.method.klass
        cp = klass.runtime_const_pool
        method_ref = cp[self.index]
        resolved_class: Class = method_ref.resolved_class()
        resolved_method: Method = method_ref.resolved_method()
        if resolved_method.name == 'java/lang/Object':
            return

        # print('invoke %s %s.%s' % (
        #     resolved_method.descriptor, resolved_class.name, resolved_method.name,))

        if resolved_method.name == '<init>' and resolved_method.klass is not resolved_class:
            raise NoSuchMethodError()

        if resolved_method.is_static:
            raise IncompatibleClassChangeError()

        ref = frame.operand_stack[-len(resolved_method.parameter_types) - 1]
        if ref is None:
            raise NullPointException()

        if resolved_method.is_protected and resolved_method.klass.is_super_class_of(klass) \
                and resolved_method.klass.package_name == klass.package_name \
                and ref.klass is not klass and not ref.klass.is_sub_class_of(klass):
            raise IllegalAccessError()

        invoking_method = resolved_method
        if klass.is_super and resolved_class.is_super_class_of(klass) \
                and resolved_method.name != '<init>':
            invoking_method = lookup_method_in_class(klass.super_class,
                                                     method_ref.name, method_ref.descriptor)

        if invoking_method is None or invoking_method.is_abstract:
            raise AbstractMethodError()
        invoke_method(frame, invoking_method)

        self.record = 'INVOKE_SPECIAL %s %s.%s(%s)' % (invoking_method.return_type,
                                                       invoking_method.klass.name, invoking_method.name,
                                                       ','.join(invoking_method.parameter_types))


class CHECK_CAST(Index16Instruction):

    def execute(self, frame: Frame):
        stack = frame.operand_stack
        ref = stack[-1]
        if ref is None:
            return
        cp = frame.method.klass.runtime_const_pool
        class_ref = cp[self.index]
        klass = class_ref.resolved_class()
        if not ref.instance_of(klass):
            raise ClassCastException()


class GET_FIELD(Index16Instruction):

    def execute(self, frame: Frame):
        cp = frame.method.klass.runtime_const_pool
        field_ref = cp[self.index]
        field = field_ref.resolved_field()

        if field.is_static:
            raise IncompatibleClassChangeError()

        stack = frame.operand_stack
        ref = stack.pop()
        if ref is None:
            raise NullPointException()

        val = ref.value_of(field)
        stack.push(val)


class GET_STATIC(Index16Instruction):

    def execute(self, frame: Frame):
        cp = frame.method.klass.runtime_const_pool
        field_ref = cp[self.index]
        field = field_ref.resolved_field()
        klass = field.klass
        if goto_init_class(klass, frame):
            return

        if not field.is_static:
            raise IncompatibleClassChangeError()

        val = klass.get_static_val(field)
        frame.operand_stack.push(val)


class INSTANCE_OF(Index16Instruction):

    def execute(self, frame: Frame):
        stack = frame.operand_stack
        ref = stack.pop()

        if ref is None:
            stack.push(0)
            return

        cp = frame.method.klass.runtime_const_pool
        class_ref = cp[self.index]
        klass = class_ref.resolved_class()
        stack.push(1 if ref.instance_of(klass) else 0)


class INVOKE_INTERFACE(Instruction):

    def __init__(self):
        Instruction.__init__(self)
        self.index = None

    def fetch_operands(self, reader: BytecodeReader):
        self.index = reader.int16()
        reader.u1()
        reader.u1()

    def execute(self, frame: Frame):
        cp = frame.method.klass.runtime_const_pool
        method_ref = cp[self.index]
        method = method_ref.resolved_method()

        if method.is_static or method.is_private:
            raise IncompatibleClassChangeError()

        ref = frame.operand_stack[-len(method.parameter_types) - 1]
        if ref is None:
            raise NullPointException()
        if not ref.klass.is_implements(method.klass):
            raise IncompatibleClassChangeError()

        invoking_method = lookup_method_in_class(ref.klass, method_ref.name,
                                                 method_ref.descriptor)
        if invoking_method is None or invoking_method.is_abstract:
            raise AbstractMethodError()

        if not invoking_method.is_public:
            raise IllegalAccessError()

        invoke_method(frame, invoking_method)
        self.record = 'INVOKE_STATIC %s %s.%s(%s)' % (invoking_method.return_type,
                                                      invoking_method.klass.name, invoking_method.name,
                                                      ','.join(invoking_method.parameter_types))


class MONITOR_ENTER(Instruction):

    def execute(self, frame: Frame):
        ref = frame.operand_stack.pop()
        if ref is None:
            raise NullPointException()
        # todo


class MONITOR_EXIT(Instruction):

    def execute(self, frame: Frame):
        ref = frame.operand_stack.pop()
        if ref is None:
            raise NullPointException()
        # todo


class MULTI_ANEW_ARRAY(Instruction):

    def __init__(self):
        Instruction.__init__(self)
        self.index = None
        self.dimensions = None

    def fetch_operands(self, reader: BytecodeReader):
        self.index = reader.int16()
        self.dimensions = reader.int8()

    def execute(self, frame: Frame):
        cp = frame.method.klass.runtime_const_pool
        class_ref = cp[self.index]
        arr_class = class_ref.resolved_class()

        stack = frame.operand_stack
        lens = MULTI_ANEW_ARRAY.pop_lens(stack, self.dimensions)
        arr_ref = MULTI_ANEW_ARRAY.new_mul_dimensional_arr(lens, arr_class)
        stack.push(arr_ref)

    @staticmethod
    def pop_lens(stack: OperandStack, dimensions: int) -> list:
        lens = [0] * dimensions
        for i in range(dimensions - 1, -1, -1):
            length = stack.pop()
            if length < 0:
                raise NegativeArraySizeException()
            lens[i] = length
        return lens

    @staticmethod
    def new_mul_dimensional_arr(lens: list, arr_class: Class):

        length = lens[0]
        arr_ref = Heap.new_array(arr_class, length)
        if len(lens) > 1:
            for i in range(len(arr_ref.data)):
                arr_ref.data[i] = MULTI_ANEW_ARRAY.new_mul_dimensional_arr(
                    lens[1:], arr_class.loader.get_component_class(arr_class)
                )

        return arr_ref


class PUT_FIELD(Index16Instruction):

    def execute(self, frame: Frame):
        cur_method = frame.method
        cur_class = cur_method.klass
        cp = cur_class.runtime_const_pool
        field_ref = cp[self.index]
        field = field_ref.resolved_field()

        if field.is_static:
            raise IncompatibleClassChangeError()

        if field.is_final:
            if cur_class is not field.klass or cur_method.name != '<init>':
                raise IllegalAccessError()

        stack = frame.operand_stack

        val = stack.pop()
        ref = stack.pop()
        if ref is None:
            raise NullPointException()

        ref.put(field, val)


class PUT_STATIC(Index16Instruction):

    def execute(self, frame: Frame):
        cur_method = frame.method
        cur_class = cur_method.klass
        cp = cur_class.runtime_const_pool
        field_ref = cp[self.index]
        field = field_ref.resolved_field()
        klass = field.klass
        if goto_init_class(klass, frame):
            return

        if not field.is_static:
            raise IncompatibleClassChangeError()

        if field.is_final:
            if cur_class is not klass or cur_method.name != '<clinit>':
                raise IllegalAccessError()

        stack = frame.operand_stack
        val = stack.pop()
        klass.put_static_field(field, val)


class INVOKE_DYNAMIC(Instruction):
    pass  # todo


class ARRAY_LENGTH(Instruction):

    def execute(self, frame: Frame):
        stack = frame.operand_stack
        ref = stack.pop()
        if ref is None:
            raise NullPointException()
        stack.push(len(ref.data))


__all__ = ['INVOKE_DYNAMIC', 'INVOKE_INTERFACE', 'INVOKE_SPECIAL', 'INVOKE_STATIC', 'INVOKE_VIRTUAL',
           'NEW', 'NEW_ARRAY', 'ANEW_ARRAY', 'ARRAY_LENGTH', 'MONITOR_ENTER', 'MONITOR_EXIT',
           'INSTANCE_OF', 'MULTI_ANEW_ARRAY', 'GET_STATIC', 'PUT_STATIC', 'PUT_FIELD',
           'GET_FIELD', 'ATHROW', 'CHECK_CAST']
